package visualizer.client.widgets.tree;

import com.google.gwt.animation.client.Animation;
import com.google.gwt.canvas.client.Canvas;
import com.google.gwt.canvas.dom.client.Context2d;
import com.google.gwt.core.client.GWT;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.SimplePanel;
import visualizer.client.bst.BNode;
import visualizer.client.bst.BinaryTree;
import visualizer.client.utils.ScaleInfo;
import visualizer.client.utils.VisualizerUtils;

import java.util.HashMap;
import java.util.Map;

/**
 * User: DDovzhenko
 * Date: 28.12.13
 * Time: 14:37
 */
public class TreeView extends Composite implements TreePresenter.Display {
    public static final int WIDTH_DELTA  = 50;
    public static final int HEIGHT_DELTA = 40;
    public static final int START_Y = 50;
    public static final String BG_COLOR = "#F0F6FA";
    public static final int CIRCLE_RADIUS = 20;
    public static final int MAX_CIRCLE_RADIUS = 25;

    @UiField
    SimplePanel tree;

    private Canvas canvas;
    private Context2d ctx;
    private double width;
    private double height;
    private double defaultWidth;
    private double defaultHeight;
    private double circleRadius;
    private double lineWidth;

    private double scale;
    private BinaryTree lastTree;
    private TreeAnimation treeAnimation = new TreeAnimation();

    public TreeView(double width, double height) {
        initWidget(ourUiBinder.createAndBindUi(this));
        this.defaultWidth = width;
        this.defaultHeight = height;

        initCanvas();
        if (canvas != null)  {
            tree.setWidget(canvas);
        }
    }

    private void initCanvas() {
        canvas = Canvas.createIfSupported();
        updateCanvasSize();
        ctx = canvas.getContext2d();
    }

    @Override
    public void setNewTree(BinaryTree tree) {
        calculateParameters();
        lastTree = tree;
        render();
    }

    @Override
    public void updateTree(BinaryTree tree) {
        calculateParameters();
        treeAnimation.cancel();
        treeAnimation.setNewTree(tree);
        treeAnimation.run(VisualizerUtils.animationSpeed);
    }

    @Override
    public void onResize() {
        updateCanvasSize();
        render();
    }

    private void updateCanvasSize() {
        scale = ScaleInfo.getInstance().getScale();
        height = defaultHeight * scale;
        width = Window.getClientWidth();
        canvas.setWidth(width + "px");
        canvas.setCoordinateSpaceWidth((int) width);

        canvas.setHeight(height + "px");
        canvas.setCoordinateSpaceHeight((int) height);
    }


    private void render() {
        prepareBackground();
        calculateParameters();
        drawTree(makeTreePoints(lastTree.getRoot()));
    }

    private void drawTree(Map<String, TreePoint> points) {
        for (TreePoint point : points.values()) {
            drawBranch(point, point.getLeftChild());
            drawBranch(point, point.getRightChild());
        }
        for (TreePoint point : points.values()) {
            drawPoint(point);
        }
    }

    private void prepareBackground() {
        ctx.clearRect(0, 0, defaultWidth, defaultHeight);
        ctx.setFillStyle(BG_COLOR);
        ctx.fillRect(0, 0, width, height);
    }

    private void calculateParameters() {
        circleRadius = CIRCLE_RADIUS * scale;
        if (circleRadius > MAX_CIRCLE_RADIUS) {
            circleRadius = MAX_CIRCLE_RADIUS;
        }
        lineWidth = circleRadius / 7;
    }

    private Map<String, TreePoint> makeTreePoints(BNode root) {
        Map<String, TreePoint> points = new HashMap<String, TreePoint>();
        makeNodeMap(root, points);
        if (root == null) {
            return points;
        }
        TreePoint rootPoint = points.get(root.getValue().toString());
        calculateWidths(rootPoint);
        int startX = (int) (width / 2);
        if (rootPoint.getLeftWidth() > startX) {
            startX = rootPoint.getLeftWidth();
        } else if (rootPoint.getRightWidth() > startX) {
            startX = Math.max(rootPoint.getLeftWidth(), 2 * startX - rootPoint.getRightWidth());
        }
        setNewPositions(rootPoint, startX, START_Y, 0);
        return points;
    }

    private void setNewPositions(TreePoint root, int x, int y, int side) {
        if (root != null) {
            root.setY(y);
            if (side == -1) {
                x = x - root.getRightWidth();
            } else if (side == 1) {
                x = x + root.getLeftWidth();
            }
            root.setX(x);
            setNewPositions(root.getLeftChild(), x, y + HEIGHT_DELTA, -1);
            setNewPositions(root.getRightChild(), x, y + HEIGHT_DELTA, 1);
        }
    }

    private TreePoint makeNodeMap(BNode root, Map<String, TreePoint> points) {
        if (root != null) {
            TreePoint point = makeTreePoint(root);
            point.setLeftChild(makeNodeMap(root.getLeftBNode(), points));
            point.setRightChild(makeNodeMap(root.getRightBNode(), points));
            points.put(point.getValue(), point);
            return point;
        }
        return null;
    }

    private int calculateWidths(TreePoint point) {
        if (point == null) {
            return 0;
        }
        point.setLeftWidth(Math.max(calculateWidths(point.getLeftChild()), WIDTH_DELTA / 2));
        point.setRightWidth(Math.max(calculateWidths(point.getRightChild()), WIDTH_DELTA / 2));
        return point.getRightWidth() + point.getLeftWidth();
    }

    private TreePoint makeTreePoint(BNode node) {
        TreePoint treePoint = new TreePoint();
        treePoint.setInnerColor(node.getInnerColor());
        treePoint.setOuterColor(node.getOuterColor());
        treePoint.setValue(String.valueOf(node.getValue()));
        treePoint.setRadius(circleRadius);
        treePoint.setVisible(node.isVisible());
        return treePoint;
    }

    private void drawPoint(TreePoint point) {
        if (!point.isVisible()) {
            return;
        }
        double x = point.getX();
        double y = point.getY() * scale;

        ctx.beginPath();
        ctx.setLineWidth(point.getOuterColor().equals(VisualizerUtils.DEFAULT_OUTER_COLOR) ? 1 : lineWidth);
        ctx.setFillStyle(point.getInnerColor());
        ctx.setStrokeStyle(point.getOuterColor());
        ctx.arc(x, y, point.getRadius(), 0, 2 * Math.PI);
        ctx.fill();
        ctx.stroke();

        ctx.beginPath();
        ctx.setFillStyle("#000000");
        ctx.setFont(circleRadius + "px sans-serif");
        String text = point.getValue();
        double width = ctx.measureText(text).getWidth();
        double height = ctx.measureText("w").getWidth();
        ctx.fillText(text, x - width / 2, y + height / 2);
    }

    private void drawBranch(TreePoint pointFrom, TreePoint pointTo) {
        if (pointTo == null || !pointFrom.isVisible() || !pointTo.isVisible()) {
            return;
        }
        ctx.beginPath();
        ctx.moveTo(pointFrom.getX(), pointFrom.getY() * scale);
        ctx.lineTo(pointTo.getX(), pointTo.getY() * scale);
        ctx.setStrokeStyle("#94B8FF");
        ctx.setLineWidth(lineWidth);
        ctx.stroke();
    }

    private class TreeAnimation extends Animation {
        private BinaryTree newTree;
        private Map<String,TreePoint> oldPoints;

        private void setNewTree(BinaryTree newTree) {
            this.newTree = newTree;
            oldPoints = makeTreePoints(lastTree.getRoot());
        }

        @Override
        protected void onUpdate(double progress) {
            Map<String, TreePoint> newPoints = makeTreePoints(newTree.getRoot());
            calculateNewPoints(progress, oldPoints, newPoints);
            prepareBackground();
            drawTree(newPoints);
        }

        private void calculateNewPoints(double progress, Map<String, TreePoint> oldPoints, Map<String, TreePoint> newPoints) {
            for (TreePoint point : newPoints.values()) {
                TreePoint oldPoint = oldPoints.get(point.getValue());
                if (oldPoint == null) {
                    continue;
                }
                Double xOffset = progress * Math.abs(point.getX() - oldPoint.getX());
                Double yOffset = progress * Math.abs(point.getY() - oldPoint.getY());
                Double newX = (point.getX() > oldPoint.getX())
                        ? (oldPoint.getX() + xOffset)
                        : (oldPoint.getX() - xOffset);
                Double newY = (point.getY() > oldPoint.getY())
                        ? (oldPoint.getY() + yOffset)
                        : (oldPoint.getY() - yOffset);
                point.setX(newX);
                point.setY(newY);

                if (!point.getOuterColor().equals(oldPoint.getOuterColor()) && !point.getOuterColor().equals(VisualizerUtils.DEFAULT_OUTER_COLOR)) {
                    double radiusProgress = (progress < 0.5) ? progress : (1 - progress);
                    double newRadius = circleRadius + circleRadius * 0.5 * radiusProgress;
                    point.setRadius(newRadius);
                }
            }
        }

        @Override
        protected void onComplete() {
            super.onComplete();
            lastTree = newTree;
            render();
        }
    }

    //------------- UI Binder --------------
    interface TreeViewUiBinder extends UiBinder<HTMLPanel, TreeView> { }
    private static TreeViewUiBinder ourUiBinder = GWT.create(TreeViewUiBinder.class);
}